package scalab.core.interpreter

import scalab.core.component.Task

/**
*	This is the interpreter's preprocessor. It aims at 
*	handling includes in a scala files. More precisely, 
*	the preprocessor receives one single scala input 
*	dependency and replaces all include statements 
* by the content of the specified file. This behavior 
*	is recursive.
* include statements must either lie in a preprocessed 
*	block or be prefixed by '#'.
* preprocessed blocks start with '/#' and end with '#/'
* remark also that all statements must begin a new line.<br>
*	Here are some examples:
*	<code>Scala code<br>
*	#include ./file.scala<br>
*	Scala code<br>
*	#include ./{f1.scala, f2.scala}<br>
*	Scala code<br>
*	/#<br>
*	include ./file.scala<br>
*	include ./{<br>
*		f1.scala,<br>
*		f2.scala<br>
*	}<br>
*	#/</code><br>
*
* Moreover, files are included only once which resolves 
*	also cyclic includes. 
* Note that there are some problems:
*	<ul><li>line directly after a statement block is gobbled</li>
*	<li>should implement an accurate conservative cache</li></ul>
*/
trait Preprocessor extends Task{

	import java.io.{Writer, PrintWriter, LineNumberReader, BufferedWriter, 
		IOException, FileReader, FileWriter, FileNotFoundException, File}

	import scalab.core.Resource
	import scala.collection.immutable.{Set, HashSet}
	import scalab.utils.StringUtils

	/**
	*	the build process' main scala file
	*/
	val mainBuildFile = mandatoryIn
	/**
	*	the preprocessed destination file
	*/
	val destinationFile: String
	/**
	*	The set of all included files. This field is used to avoid 
	*	cyclic includes.
	*/
	var includedFiles: Set[String] = _

	/**
	*	The entry point of the task.
	*/
	def execute(runDir: String): Boolean = {
		val deps = mainBuildFile.sources.toList
		if(deps.isEmpty){
			error("I did not get any build file!")
			false
		}
		else if(!deps.tail.isEmpty){
			error("I got more than one build file! If you want to " +
				"separate your build process in multiple files, include " +
				"those files in a common main build file.")
			false
		}
		else if(deps.head.isDirectory){
			error(deps.head.getPath + " is a directory!")
			false
		}
		else{
		
			val writer = try{
				new PrintWriter(new BufferedWriter(new FileWriter(runDir + destinationFile)))
				}
				catch{
					case ioe: IOException => 
						error(ioe.getMessage)
						null
				}
			if(writer eq null)return false
	
			try{
				includedFiles = HashSet.empty[String]
				writer.println("import scalab.core.component._")
				writer.println("import scalab.core.dependencies._")
				writer.println("import scalab.core.event._")
				writer.println("import scalab.core.interpreter._")
				writer.println("import scalab.core.utils._")
				writer.println("import scalab.core.gates._")
				writer.println("object BuildGraph extends Build{")
				
				preprocess("." + File.separator, deps.head, writer)
				writer.println("}")
			}
			finally{
				if(writer ne null)
					writer.close
			}

			if(errors > 0){
				error(errors + " error" + (if(errors == 1) "" else "s") + " found!")
			}
			else
				built(runDir + destinationFile)

			errors == 0
		}
	}

	/**
	*	Writes in <code>writer</code> the content of <code>file</code> 
	*	with include statements replaced by the content of the 
	*	concerned file and recursively.
	*/
	def preprocess(workingDirectory: String, file: Resource, writer: PrintWriter){
		
		val canonicalPath = file.getCanonicalPath
		if(!includedFiles.contains(canonicalPath)){ //avoids cyclic includes
			includedFiles = includedFiles + canonicalPath
			val reader = try{
					new LineNumberReader(new FileReader(file))
				}
				catch{
					case fnfe: FileNotFoundException => 
						fail(fnfe.getMessage)
						null
				}
			reader setLineNumber 1 //count from 1 instead of 0

			try{
				var line: String = reader.readLine
				while(line ne null){

					line = preprocessStatements(workingDirectory, line, file, reader, writer)
					if(line ne null)
						writer.println(line)
					line = reader.readLine
				}
			}
			finally{
				if(reader ne null)
					reader.close
			}
		}
	}

	/**
	*	Reads the input, preprocesses statements until the next scala code block and returns 
	*	the first line of this scala code block or null if the end of the file was reached.
	*/
	def preprocessStatements(workingDirectory: String, firstLine: String, file: Resource, reader: LineNumberReader, writer: PrintWriter): String = {
		var line = firstLine
		var statements: List[(String, Int)] = Nil
		

		var newStats = fetchStatements(line, file, reader)
		
		while(!newStats.isEmpty){
			newStats foreach {s => statements = s :: statements}
			//statements = (statementOpt.get, reader.getLineNumber) :: statements
			
			line = reader.readLine
			newStats = fetchStatements(line, file, reader)
		}
		//info("reversed stats: " + statements)
		getIncludedFiles(workingDirectory, statements, file, reader) foreach {f => preprocess(f.getParent + File.separator, f, writer)}
		line
	}

	/**
	*	Transforms a list of statements (a list of (path, line number) pairs) into a list of 
	*	resources to be included.
	*/
	def getIncludedFiles(workingDirectory: String, statements: Iterable[(String, Int)], file: Resource, reader: LineNumberReader): List[Resource] = {
		var files: List[Resource] = Nil
		val it = statements.elements
		var s: (String, Int) = null
		//info("incFiles stats:" + statements)
		while(it.hasNext){
			s = it.next
			if(!s._1.startsWith(Preprocessor.includeStatement)){
				val prefix = if(s._1.length > 10) s._1.substring(10) + "..." else s._1
				error("Unknown statement: '" + prefix + "' in file " + file.canonicalPath + ", line " + s._2 + "!")
			}
			else{
				val statement = StringUtils.trim(s._1.substring(Preprocessor.includeLength))
				val pathSeqStartId = statement indexOf Preprocessor.leftPathSequenceSep
				
				if(pathSeqStartId != -1){ //multiple pathes
					val basePath = statement.substring(0, pathSeqStartId)
					
					var paths: Array[String] = statement.substring(pathSeqStartId + 1).split(Preprocessor.pathSeparator)
					
					var closed = false
					var hasNext = true
					
					while(!closed && hasNext){
						//info("passe")
						val lastId = paths.length - 1
						closed = paths(lastId).endsWith(Preprocessor.rightPathSequenceSep)
						
						if(closed)
							paths(lastId) = paths(lastId).substring(0, paths(lastId).length - 1)
							
						//info("paths: " + paths.mkString)
						paths map {p => 
							StringUtils.trim(p)} filter {p => 
								p != ""} foreach {p => 
									files = prependValidPath(workingDirectory, files, basePath + p, file, s._2)}
						hasNext = it.hasNext
						if(! closed && hasNext){
							s = it.next
							paths = s._1.split(Preprocessor.pathSeparator)
						}
					}
				}
				else //single path
					files = prependValidPath(workingDirectory, files, statement, file, s._2)
			}
		}
		requireResources(file, files)
		files.reverse
	}

	/**
	*	Increases the list of files <code>files</code> with the one denoted by <code>path<code>
	*	if the later is valid. A path is valid if its related resources exists and is a file.
	*/
	def prependValidPath(workingDirectory: String, files: List[Resource], path: String, file: Resource, lineNumber: Int): List[Resource] = {
		val f = Resource(workingDirectory + path)
		if(!f.exists){
			error("Cannot include '" + f.getPath + "' since it does not exists in file " + file.canonicalPath + ", line " + lineNumber)
			files
		}
		else if(f.isDirectory){
			error("Cannot include '" + f.getPath + "' since it is a directory in file " + file.canonicalPath + ", line " + lineNumber)
			files
		}
		else
			f :: files
	}

	/**
	* Reads a statement or block of statement and generates a list of (path, lineNumber) pairs.
	*/
	def fetchStatements(line: String, file: Resource, reader: LineNumberReader): List[(String, Int)] = {
		var newLine = line
		var statements: List[(String, Int)] = Nil
		var singleStatement = false
		var multiStatements = false
		var trimed: String = null
		if(newLine ne null){
			trimed = StringUtils.leftTrim(newLine)
			singleStatement = trimed startsWith Preprocessor.statementPrefix
			multiStatements = trimed startsWith (Preprocessor.multiLineSeparator + Preprocessor.statementPrefix)
		}
		while((newLine ne null) && (singleStatement || multiStatements)){
			if(singleStatement){
				//info("singleStatement")
				statements = (StringUtils.leftTrim(trimed.substring(Preprocessor.prefixLength)), reader.getLineNumber) :: statements
				newLine = reader.readLine
			}
			else{
				//info("multi statements")
				var closed = false
				var stat: String = null
				newLine = trimed.substring(Preprocessor.multiLineDelimLength)
				while(!closed && (newLine ne null)){
					trimed = StringUtils.trim(newLine)
					
					if(trimed endsWith (Preprocessor.statementPrefix + Preprocessor.multiLineSeparator)){
						closed = true
						stat = StringUtils.rightTrim(trimed.substring(0, trimed.length - Preprocessor.multiLineDelimLength))
					}
					else{
						stat = trimed
					}
				
					if(stat != "")
						statements = (stat, reader.getLineNumber) :: statements
					newLine = reader.readLine
				}
			}
			
			if(newLine ne null){
				trimed = StringUtils.leftTrim(newLine)
				singleStatement = trimed startsWith Preprocessor.statementPrefix
				multiStatements = trimed startsWith (Preprocessor.multiLineSeparator + Preprocessor.statementPrefix)
			}
		}

		statements
	}

	/**
	* Behavior changes if the destination file changes.
	*/
	def instanceId: String = destinationFile

	/**
	*	The total number of errors that were printed by the preprocessor so far.
	*/
	var errors: Int = 0
	/**
	*	Pretty prints an error message and increments
	*	the error count number.
	*/
	override def error(obj: Any): Boolean = {
		errors += 1
		super.error(obj)
	}
}

/**
* This object contains parameters related to the scalab preprocessor 
*	as well as an apply function that allows to instantiate an new 
*	preprocessor.
*/
object Preprocessor{

	lazy val statementPrefix = "#"
	lazy val multiLineSeparator = "/"
	lazy val prefixLength = statementPrefix.length
	lazy val multiLineDelimLength = prefixLength + multiLineSeparator.length
	lazy val includeStatement = "include"
	lazy val includeLength = includeStatement.length
	lazy val leftPathSequenceSep = '{'
	lazy val rightPathSequenceSepChar = '}'
	lazy val rightPathSequenceSep = String.valueOf(rightPathSequenceSepChar)
	
	lazy val pathSeparatorChar = ','
	lazy val pathSeparator = String.valueOf(pathSeparatorChar)

	def apply(): Preprocessor = new Preprocessor{
		val name: String = "Preprocessor"
		val destinationFile: String = "build.scala"
	}
}


